這篇文章主要敘述我用基因遺傳演算法解旅行推銷員問題,用的語言是Python。
這邊會一步一步帶過程式碼,之後也會上傳到Github。
演算法參考:https://towardsdatascience.com/evolution-of-a-salesman-a-complete-genetic-algorithm-tutorial-for-python-6fe5d2b3ca35
我大概看過上面的文章就開始動手寫了,在Crossover的地方會比較類似。其餘Code的實現跟一些調整應該還是讓這篇文章滿值得一看的。
先上成果圖:
文章架構為:
import random as rd
import copy
from matplotlib import pyplot as plt
class Location:
def __init__(self, name, x, y):
self.name = name
self.loc = (x, y)
def distance_between(self, location2):
assert isinstance(location2, Location)
return ((self.loc[0] - location2.loc[0]) ** 2 + (self.loc[1] - location2.loc[1]) ** 2) ** (1 / 2)
這邊Location指的就是城市。
class Route:
def __init__(self, path):
# path is a list of Location obj
self.path = path
self.length = self._set_length()
def _set_length(self):
total_length = 0
path_copy = self.path[:]
from_here = path_copy.pop(0)
init_node = copy.deepcopy(from_here)
while path_copy:
to_there = path_copy.pop(0)
total_length += to_there.distance_between(from_here)
from_here = copy.deepcopy(to_there)
total_length += from_here.distance_between(init_node)
return total_length
這邊Route指的就是路徑。
到這邊我們的染色體(城市)和個體(路徑)就定義完成了。
3. 定義GeneticAlgo
注意:這點的function都是定義在GeneticAlgo這個class底下的
Step1. 初始化重要參數:
class GeneticAlgo:
def __init__(self, locs, level=10, populations=100, variant=3, mutate_percent=0.01, elite_save_percent=0.1):
self.locs = locs
self.level = level
self.variant = variant
self.populations = populations
self.mutates = int(populations * mutate_percent)
self.elite = int(populations * elite_save_percent)
Step2. 建立第一代的路徑群體
def _find_path(self):
# locs is a list containing all the Location obj
locs_copy = self.locs[:]
path = []
while locs_copy:
to_there = locs_copy.pop(locs_copy.index(rd.choice(locs_copy)))
path.append(to_there)
return path
def _init_routes(self):
routes = []
for _ in range(self.populations):
path = self._find_path()
routes.append(Route(path))
return routes
這邊我們需要兩個function:
Step3. 產生下一代
def _get_next_route(self, routes):
routes.sort(key=lambda x: x.length, reverse=False)
elites = routes[:self.elite][:]
crossovers = self._crossover(elites)
return crossovers[:] + elites
def _crossover(self, elites):
# Route is a class type
normal_breeds = []
mutate_ones = []
for _ in range(self.populations - self.mutates):
father, mother = rd.choices(elites[:4], k=2)
index_start = rd.randrange(0, len(father.path) - self.variant - 1)
# list of Location obj
father_gene = father.path[index_start: index_start + self.variant]
father_gene_names = [loc.name for loc in father_gene]
mother_gene = [gene for gene in mother.path if gene.name not in father_gene_names]
mother_gene_cut = rd.randrange(1, len(mother_gene))
# create new route path
next_route_path = mother_gene[:mother_gene_cut] + father_gene + mother_gene[mother_gene_cut:]
next_route = Route(next_route_path)
# add Route obj to normal_breeds
normal_breeds.append(next_route)
# for mutate purpose
copy_father = copy.deepcopy(father)
idx = range(len(copy_father.path))
gene1, gene2 = rd.sample(idx, 2)
copy_father.path[gene1], copy_father.path[gene2] = copy_father.path[gene2], copy_father.path[gene1]
mutate_ones.append(copy_father)
mutate_breeds = rd.choices(mutate_ones, k=self.mutates)
return normal_breeds + mutate_breeds
這邊也是兩個function:
_get_next_route(self, routes):
這個函式會把一個群體(路徑們)按照他們的路徑長度做排序。(line 1)
接著取出前面幾%的路徑做為菁英群體。(line 2)
接著呼叫_crossover來取得繁衍後的子代。(line 3)
最後生成新的子代。(line 4)
注意:我們這邊為確保有更多好的基因流傳下去,我們直接讓菁英群體保送到下一代
_crossover(self, elites, total_count, mutate_count):
在談這個函式之前我們先來談談我們是如何讓兩個路徑「繁衍」出子代的。
拿兩條路徑做例子:
ㄅ路徑:A -> B -> D -> C -> E -> G -> F
ㄆ路徑:C -> B -> A -> D -> E -> F -> G
步驟1. 隨機取出ㄅ路徑中的一小截基因(基因的長度取決於self.variant),例如 BDC
步驟2. 移除ㄆ路徑中的BDC,得到ㄇ路徑:A -> E -> F -> G
步驟3. 把BDC隨機放入ㄇ路徑當中,得到新路徑:A -> E -> B -> D -> C -> F -> G
其實這麼做背後的原因很直覺,既然ㄅ路徑作為菁英路徑(較短的路徑們之一),那他之中勢必有一小段路徑或 是好幾小段的路徑是值得我們繼續使用的,因此我們希望這樣擷取這些小段的路徑作為子代的一部份可以幫助我 們產生路徑更短的子代。
注意:這麼做的原因還有:按照問題要求,我們必須經過每個要求的城市,因此每個路徑所經過的城市數量都 要相同
接著談談程式碼:
father, mother = rd.choices(elites[:4], k=2)
index_start = rd.randrange(0, len(father.path) - self.variant - 1)
# list of Location obj
father_gene = father.path[index_start: index_start + self.variant]
father_gene_names = [loc.name for loc in father_gene]
mother_gene = [gene for gene in mother.path if gene.name not in father_gene_names]
mother_gene_cut = rd.randrange(1, len(mother_gene))
# create new route path
next_route_path = mother_gene[:mother_gene_cut] + father_gene + mother_gene[mother_gene_cut:]
next_route = Route(next_route_path)
這段程式碼當中第1行我們從菁英路徑們當中的前四名隨機取出兩個作為父和母。
第2~4行我們隨機擷取父親染色體的一部分。
第6~8行我們移除母親染色體當中含有父親那小節染色體的部分,同時決定要從哪裡放入父親那小節染色體。
接著next_route_path就是子代路徑
然後我們用子代路徑建立Route物件。
再來是突變
# for mutate purpose
copy_father = copy.deepcopy(father)
idx = range(len(copy_father.path))
gene1, gene2 = rd.sample(idx, 2)
copy_father.path[gene1], copy_father.path[gene2] = copy_father.path[gene2], copy_father.path[gene1]
mutate_ones.append(copy_father)
mutate_breeds = rd.choices(mutate_ones, k=self.mutates)
在第五行中我們隨機調換父路徑當中的兩座城市作為突變,加到mutate_ones裡頭。
最後一行是因為我們最後只要self.mutates這麼多個突變個數
Step4. 進化
def evolution(self):
routes = self._init_routes()
for _ in range(self.level):
routes = self._get_next_route(routes)
routes.sort(key=lambda x: x.length)
return routes[0].path, routes[0].length
這段程式碼就是我們的演化流程。
初始化群體 -> 繁衍新群體(重複self.level次(子代代數)) -> 得到最後的群體
Step5. 執行程式
在執行以前我們必須先生成一堆隨機的城市地點。
def create_locations():
locations = []
xs = [8, 50, 18, 35, 90, 40, 84, 74, 34, 40, 60, 74]
ys = [3, 62, 0, 25, 89, 71, 7, 29, 45, 65, 69, 47]
cities = ['Z', 'P', 'A', 'K', 'O', 'Y', 'N', 'X', 'G', 'Q', 'S', 'J']
for x, y, name in zip(xs, ys, cities):
locations.append(Location(name, x, y))
return locations, xs, ys, cities
這部分我用random生成了,xs是x座標,ys是y座標,cities是城市名稱。
然後就是本體
if __name__ == '__main__':
my_locs, xs, ys, cities = create_locations()
my_algo = GeneticAlgo(my_locs, level=40, populations=150, variant=2, mutate_percent=0.02, elite_save_percent=0.15)
best_route, best_route_length = my_algo.evolution()
best_route.append(best_route[0])
print([loc.name for loc in best_route], best_route_length)
print([(loc.loc[0], loc.loc[1]) for loc in best_route], best_route_length)
這部分也不難理解,就是隨機生成城市後,讓GeneticAlgo去演化我們的可行路徑。
要注意的是因為TSP要求起點終點要一樣所以我們補上終點。
以下部分可以視覺化最終結果:
fig, ax = plt.subplots()
ax.plot([loc.loc[0] for loc in best_route], [loc.loc[1] for loc in best_route], 'red', linestyle='-', marker='')
ax.scatter(xs, ys)
for i, txt in enumerate(cities):
ax.annotate(txt, (xs[i], ys[i]))
plt.show()
其實這樣的演算法並不一定能達到最佳解,只是能在相對較短的時間內得到一個還不錯的解這樣。程式碼已經上傳到我的Github:https://github.com/blue0107/Genetic-Algorithm-for-TSP
有任何問題歡迎留言或寄信到我的信箱:b05703129@ntu.edu.tw
祝各位有美好的一天!